Skip to content

feat(onboarding): mnemonic-entry refactor + ASB UX hardening#507

Open
yasin-ce wants to merge 11 commits into
mainfrom
yasince/asb
Open

feat(onboarding): mnemonic-entry refactor + ASB UX hardening#507
yasin-ce wants to merge 11 commits into
mainfrom
yasince/asb

Conversation

@yasin-ce
Copy link
Copy Markdown
Collaborator

Summary

Stacked on #500. Four logically grouped commits cleaning up the ASB import flow:

  • style(onboarding): hero-scale shield illustration on AsbImportInfoScreen, replacing the small PWRoundIcon chip with a direct-SVG render at the same scale as WatchInfoScreen / ImportInfoScreen.
  • feat(security): usePreventScreenCapture on BackupVerificationScreen (quiz tiles include the real mnemonic words mixed with decoys).
  • feat(onboarding): extract useMnemonicWordEntry + MnemonicSuggestionBar + splitMnemonic from the 180-line slab inside ImportAccountScreen, adopted across both ImportAccountScreen and AsbImportKeyScreen. Same diff lands four hardenings on the two screens: screen-capture prevention, navigation.replace on success paths (typed mnemonic drops for GC on unmount), envelope-missing redirect on AsbImportKeyScreen, and useHeaderHeight() replacing the HEADER_HEIGHT = 50 magic.
  • fix(onboarding): sync AsbImportBackupScreen's local loadedFile to the store's envelope — when the wizard wipes the store post-import, the "Pasted backup" card now disappears so the user can't loop through Key→Backup with a stale state.

Test plan

  • pnpm pre-push --no-fail-on-error — all checks green (lint, typecheck, formatter, copyright headers, i18n, secrets)
  • onboarding-import-asb.test.tsx — 8/8 (1 new regression test for the back-nav scenario)
  • onboarding-import-algo25.test.tsx — 4/4
  • onboarding-import-hd.test.tsx — 7/7
  • backup.test.tsx — 5/5
  • useMnemonicWordEntry.spec.ts — 12 scenarios covering paste distribution (multiple separators), clipboard fallback (incl. iOS autocomplete regression), focus advancement at boundaries, error-callback wiring
  • MnemonicSuggestionBar.spec.tsx — render, empty-state hide, tap callback
  • splitMnemonic.spec.ts — 5 separator-mix variants
  • Smoke-test on device: pick ASB file → enter recovery key → select accounts → verify no back-nav loop; confirm screen capture blocked on all 3 hardened screens

Base automatically changed from yasince/rekey to main May 15, 2026 20:33
yasin-ce added 4 commits May 15, 2026 23:36
Replaces the small `PWRoundIcon` chip with a 4×xxl direct-SVG render of
shield-check, matching the hero-illustration scale used by every other
onboarding info screen (WatchInfoScreen, ImportInfoScreen, the backup
family). Tints via `theme.colors.textMain` since shield-check.svg uses
`currentColor`, so it adapts to light/dark mode automatically.

Adds a `vi.mock` for the SVG in the integration setup — jsdom chokes on
the long data URL `svgr` emits otherwise.
`BackupVerificationScreen` shows the quiz tiles whose `options` contain
the real mnemonic words (mixed with decoys). Even though the user
hasn't picked the correct order yet, an attacker who captures the
screen learns the candidate set. Aligns with the screen-capture
prevention already applied to `BackupReminderMnemonicScreen` and
`ViewPassphraseContent`.
Pulls the mnemonic-input mechanic out of ImportAccountScreen (where it
was an ~180-line slab of paste-distribution, clipboard fallback, and
suggestion derivation) into a reusable hook + util + component, then
adopts them across both import flows:

  - `useMnemonicWordEntry` — focus management, multi-separator paste
    distribution, iOS-autocomplete-safe clipboard fallback,
    wordlist-driven suggestions. Wordlist- and copy-agnostic; callers
    pass `onTooManyWords` / `onInsufficientSlots` so screen-specific
    i18n stays out.
  - `splitMnemonic` — accepts any mix of whitespace + commas.
  - `MnemonicSuggestionBar` — sticky horizontal pill bar that renders
    nothing when there are no suggestions, replaces the old
    per-row `WordSuggestionDropdown` (deleted).

Same diff also lands four orthogonal hardenings that touch the same
screens, so they're bundled here rather than fragmented:

  - Screen-capture prevention on AsbImportKeyScreen and
    ImportAccountScreen via `usePreventScreenCapture`.
  - Success-path uses `navigation.replace` so the typed mnemonic in the
    input hook's state is dropped for GC when the screen unmounts;
    back-nav from later steps no longer lands on a prefilled screen.
  - AsbImportKey redirects to the file-pick step when `envelope` is
    null (defensive; covers the rare flow where the store gets wiped
    while the screen is mounted).
  - Replaces the `HEADER_HEIGHT = 50` magic constant with
    `useHeaderHeight()` from `@react-navigation/elements` for the
    `KeyboardAvoidingView.keyboardVerticalOffset` on both screens.

Includes vitest mocks for `@react-navigation/elements.useHeaderHeight`
(throws under the headless test navigator) and unit/component/integration
test coverage for the new primitives.
After a successful import, SelectAccounts cleanup runs `reset()` on the
flow store to zero decrypted private keys. If the user then navigates
back into the backup screen (Android system back from Result, etc.),
the local "Pasted backup" card kept rendering because `loadedFile` is
React state separate from the store. Tapping Next jumped to a Key
screen with no envelope, which bounced back here — a silent loop.

Sync `loadedFile` to `envelope`: when the store is wiped externally,
clear the local indicator so Continue properly disables and the user
has to re-pick / re-paste.

Adds a regression integration test that drives the wipe through the
same `useAsbImportFlowStore.reset()` SelectAccounts cleanup performs
(no need to simulate the full stack-nav cascade through Result).
@yasin-ce yasin-ce self-assigned this May 18, 2026
yasin-ce added 7 commits May 18, 2026 20:14
Pressing the on-screen keyboard's action button on the 12-word ASB key
and 24-word HD import screens now jumps to the next input. Inputs
1..N-1 expose returnKeyType="next" with blurOnSubmit=false so the
keyboard stays up while focus advances; the last input exposes
returnKeyType="done" and blurs on submit.

The ref array, focus-on-focused effect, and submit handler all live
in useMnemonicWordEntry so both consumers wire identically through
refCallbacks + handleSubmitEditing. The effect skips its mount run
to avoid fighting the screens' autoFocus on slot 0.
The hook calls navigation.replace (so the input screen unmounts and
its typed mnemonic is dropped for GC); the test was asserting against
push and silently failing.
splitMnemonic strips commas, but the prior check normalized only
whitespace. A keyboard that appended a comma after accepting a
suggestion ("abandon,") then failed the wordlist match, the
clipboard fallback engaged, and any mnemonic on the clipboard
overwrote every slot. Derive the candidate token from
splitMnemonic so trailing punctuation is stripped before the
wordlist check.
…cope

useImportAccountScreen.canImport intentionally only checks non-empty
slots — strict wordlist validation lives in useImportAccount and
surfaces typed errors via toast. ASB pre-validates because a wrong
mnemonic decrypts to opaque garbage rather than a typed error.
Move type-only imports into the canonical trailing block in
useMnemonicWordEntry and useAsbImportKeyScreen so type-vs-value
groups are visually distinct.

Also annotate the finally clause in handleContinue: no explicit
wipe is a deliberate UX call — success path drops the reference via
navigation.replace, error path preserves words so the user can fix
a typo without re-typing 12 words.
@yasin-ce yasin-ce marked this pull request as ready for review May 18, 2026 20:24
@yasin-ce yasin-ce requested a review from a team as a code owner May 18, 2026 20:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant